﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using GBricks.Properties;

namespace GBricks.Collections
{
  public class ComparerBuilder<T>
  {
    private const string DefaultPropertyName = "Default";

    private static readonly ConstantExpression Null = Expression.Constant(null);
    private static readonly ConstantExpression Zero = Expression.Constant(0);

    private static readonly ConstantExpression One = Expression.Constant(1);
    private static readonly ConstantExpression MinusOne = Expression.Constant(-1);

    private static readonly ParameterExpression Left = Expression.Parameter(typeof(T), null);
    private static readonly ParameterExpression Right = Expression.Parameter(typeof(T), null);

    private static readonly ParameterExpression Compare = Expression.Parameter(typeof(int));
    private static readonly IEnumerable<ParameterExpression> CompareValiables = Enumerable.Repeat(Compare, 1);
    private static readonly LabelTarget Return = Expression.Label(typeof(int));
    private static readonly LabelExpression LabelZero = Expression.Label(Return, Zero);
    private static readonly GotoExpression ReturnZero = Expression.Return(Return, Zero);
    private static readonly GotoExpression ReturnOne = Expression.Return(Return, One);
    private static readonly GotoExpression ReturnMinusOne = Expression.Return(Return, MinusOne);
    private static readonly GotoExpression ReturnCompare = Expression.Return(Return, Compare);

    private static readonly Func<object, object, bool> ObjectEqualsDelegate = Object.Equals;
    private static readonly Func<int> GetHashCodeDelegate = new object().GetHashCode;
    private static readonly Func<object, object, int> CompareDelegate = Comparer<object>.Default.Compare;
    private static readonly Func<int, int, int> RotateRightDelegate = HashCode.RotateRight;

    private static readonly string EqualsMethodName = ObjectEqualsDelegate.Method.Name;
    private static readonly string GetHashCodeMethodName = GetHashCodeDelegate.Method.Name;
    private static readonly string CompareMethodName = CompareDelegate.Method.Name;

    private static readonly bool IsValueType = typeof(T).IsValueType;

    public ComparerBuilder() {
      EqualsExpressions = new List<Expression>();
      GetHashCodeExpressions = new List<Expression>();
      CompareExpressions = new List<Expression>();
    }

    private IList<Expression> EqualsExpressions { get; set; }
    private IList<Expression> GetHashCodeExpressions { get; set; }
    private IList<Expression> CompareExpressions { get; set; }

    [ContractInvariantMethod]
    private void Invariant() {
      Contract.Invariant(EqualsExpressions != null);
      Contract.Invariant(GetHashCodeExpressions != null);
      Contract.Invariant(CompareExpressions != null);
      Contract.Invariant(EqualsExpressions.Count == GetHashCodeExpressions.Count);
      Contract.Invariant(!EqualsExpressions.Any() || Contract.ForAll(EqualsExpressions, item => item != null));
      Contract.Invariant(!GetHashCodeExpressions.Any() || Contract.ForAll(GetHashCodeExpressions, item => item != null));
      Contract.Invariant(!CompareExpressions.Any() || Contract.ForAll(CompareExpressions, item => item != null));
    }

    #region Expression Helpers

    private static BinaryExpression ReferenceEqual(Expression left, Expression right) {
      Argument.NotNull(left, "left");
      Argument.NotNull(right, "right");
      Contract.Ensures(Contract.Result<BinaryExpression>() != null);

      var result = Expression.ReferenceEqual(left, right);
      Contract.Assume(result != null);
      return result;
    }

    private static BinaryExpression IsNull(Expression value) {
      Argument.NotNull(value, "value");
      Contract.Ensures(Contract.Result<BinaryExpression>() != null);

      var result = Expression.ReferenceEqual(value, Null);
      Contract.Assume(result != null);
      return result;
    }

    private static BinaryExpression IsNotNull(Expression value) {
      Argument.NotNull(value, "value");
      Contract.Ensures(Contract.Result<BinaryExpression>() != null);

      var result = Expression.ReferenceNotEqual(value, Null);
      Contract.Assume(result != null);
      return result;
    }

    private static ConstantExpression NullableConstant(object value) {
      return value != null ? Expression.Constant(value) : null;
    }

    private static Expression CallComparerMethod(Expression comparer, string methodName, params Expression[] arguments) {
      Argument.NotNull(comparer, "comparer");
      Contract.Ensures(Contract.Result<Expression>() != null);

      const BindingFlags MethodLookup = BindingFlags.Public | BindingFlags.Instance;

      var types = arguments != null && arguments.Any() ? Array.ConvertAll(arguments, item => item.Type) : Type.EmptyTypes;
      var method = comparer.Type.GetMethod(methodName, MethodLookup, null, types, null);
      if(method == null) {
        throw ExceptionBuilder.Argument("methodName", Resources.MethodNameDoesNotFound2, methodName, comparer.Type);
      }//if

      return Expression.Call(comparer, method, arguments);
    }

    private static Expression GetDefaultComparer(Type comparerType, Type comparisonType) {
      Argument.NotNull(comparerType, "comparerType");
      Argument.NotNull(comparisonType, "comparisonType");
      Contract.Ensures(Contract.Result<Expression>() != null);

      Contract.Assume(comparerType.IsGenericTypeDefinition);
      Contract.Assume(comparerType.GetGenericArguments().Length == 1);
      var property = comparerType.MakeGenericType(comparisonType).GetProperty(DefaultPropertyName);
      Contract.Assume(property != null);

      var value = property.GetValue(null, null);
      Contract.Assume(value != null);
      return Expression.Constant(value);
    }

    #endregion Expression Helpers

    protected virtual Expression MakeEquals(Expression left, Expression right, Expression comparer) {
      Argument.NotNull(left, "left");
      Argument.NotNull(right, "right");

      if(comparer != null) {
        // return comparer.Equals(left, right);
        return CallComparerMethod(comparer, EqualsMethodName, left, right);
      } else {
        if(left.Type.IsValueType && right.Type.IsValueType) {
          // return left == right;
          return Expression.Equal(left, right);
        } else {
          // return Object.Equals(left, right);
          return Expression.Call(ObjectEqualsDelegate.Method, left, right);
        }//if
      }//if
    }

    protected virtual Expression MakeGetHashCode(Expression value, Expression comparer) {
      Argument.NotNull(value, "value");

      if(comparer != null) {
        // return comparer.GetHashCode(value);
        return CallComparerMethod(comparer, GetHashCodeMethodName, value);
      } else {
        var call = Expression.Call(value, GetHashCodeDelegate.Method);
        if(value.Type.IsValueType) {
          // return value.GetHashCode();
          return call;
        } else {
          // return (value != null ? value.GetHashCode(); : 0
          return Expression.Condition(IsNotNull(value), call, Zero);
        }//if
      }//if
    }

    protected virtual Expression MakeCompare(Expression left, Expression right, Expression comparer) {
      Argument.NotNull(left, "left");
      Argument.NotNull(right, "right");

      if(comparer != null) {
        // return comparer.Compare(left, right);
        return CallComparerMethod(comparer, CompareMethodName, left, right);
      } else {
        // return (left < right) ? -1 : (left > right ? 1 : 0);
        return Expression.Condition(Expression.LessThan(left, right), MinusOne, Expression.Condition(Expression.GreaterThan(left, right), One, Zero));
      }//if
    }

    private void AddEqualityCore(Tuple<Expression, Expression> args, Expression comparer) {
      Argument.NotNull(args, "args");

      var equals = MakeEquals(args.Item1, args.Item2, comparer);
      EqualsExpressions.Add(equals);

      var hash = MakeGetHashCode(args.Item1, comparer);
      GetHashCodeExpressions.Add(hash);
    }

    private void AddComparisonCore(Tuple<Expression, Expression> args, Expression comparer) {
      Argument.NotNull(args, "args");

      var compare = MakeCompare(args.Item1, args.Item2, comparer);
      CompareExpressions.Add(compare);
    }

    private sealed class ReplaceVisitor : ExpressionVisitor
    {
      public ReplaceVisitor(Expression what, Expression to) {
        Argument.NotNull(what, "what");
        Argument.NotNull(to, "to");

        What = what;
        To = to;
      }

      public Expression What { get; private set; }
      public Expression To { get; private set; }

      public override Expression Visit(Expression node) {
        if(node == What) {
          return To;
        }//if

        return base.Visit(node);
      }
    }

    private static Tuple<Expression, Expression> Parameters(LambdaExpression expression) {
      Argument.NotNull(expression, "expression");
      Argument.Check(expression.Parameters.Count == 1, "expression", "expression.Parameters.Count != 1");

      var left = new ReplaceVisitor(expression.Parameters[0], Left);
      var right = new ReplaceVisitor(expression.Parameters[0], Right);
      return Tuple.Create(left.Visit(expression.Body), right.Visit(expression.Body));
    }

    public ComparerBuilder<T> Add(LambdaExpression expression, Expression equality = null, Expression comparison = null) {
      Argument.NotNull(expression, "expression");
      Contract.Ensures(Contract.Result<ComparerBuilder<T>>() != null);

      var args = Parameters(expression);
      AddEqualityCore(args, equality);
      AddComparisonCore(args, comparison);
      return this;
    }

    public ComparerBuilder<T> AddDefault(LambdaExpression expression) {
      Argument.NotNull(expression, "expression");
      Contract.Ensures(Contract.Result<ComparerBuilder<T>>() != null);

      var type = expression.ReturnType;
      var equality = GetDefaultComparer(typeof(EqualityComparer<>), type);
      var comparison = GetDefaultComparer(typeof(Comparer<>), type);
      return Add(expression, equality, comparison);
    }

    public ComparerBuilder<T> AddEquality(LambdaExpression expression, Expression equality = null) {
      Argument.NotNull(expression, "expression");
      Contract.Ensures(Contract.Result<ComparerBuilder<T>>() != null);

      var args = Parameters(expression);
      AddEqualityCore(args, equality);
      return this;
    }

    public ComparerBuilder<T> AddComparison(LambdaExpression expression, Expression comparison = null) {
      Argument.NotNull(expression, "expression");
      Contract.Ensures(Contract.Result<ComparerBuilder<T>>() != null);
      
      var args = Parameters(expression);
      AddComparisonCore(args, comparison);
      return this;
    }

    [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = Justifications.TypedExpressionRequired)]
    public ComparerBuilder<T> Add<TProperty>(Expression<Func<T, TProperty>> expression, IEqualityComparer<TProperty> equality = null, IComparer<TProperty> comparison = null) {
      Argument.NotNull(expression, "expression");
      Contract.Ensures(Contract.Result<ComparerBuilder<T>>() != null);
      
      return Add(expression, NullableConstant(equality), NullableConstant(comparison));
    }

    [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = Justifications.TypedExpressionRequired)]
    public ComparerBuilder<T> Add<TProperty, TComparer>(Expression<Func<T, TProperty>> expression, TComparer comparer) where TComparer : IEqualityComparer<TProperty>, IComparer<TProperty> {
      Argument.NotNull(expression, "expression");
      Contract.Ensures(Contract.Result<ComparerBuilder<T>>() != null);
      
      var constant = NullableConstant(comparer);
      return Add(expression, constant, constant);
    }

    [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = Justifications.TypedExpressionRequired)]
    public ComparerBuilder<T> AddEquality<TProperty>(Expression<Func<T, TProperty>> expression, IEqualityComparer<TProperty> equality = null) {
      Argument.NotNull(expression, "expression");
      Contract.Ensures(Contract.Result<ComparerBuilder<T>>() != null);
      
      return AddEquality(expression, NullableConstant(equality));
    }

    [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = Justifications.TypedExpressionRequired)]
    public ComparerBuilder<T> AddComparison<TProperty>(Expression<Func<T, TProperty>> expression, IComparer<TProperty> comparison = null) {
      Argument.NotNull(expression, "expression");
      Contract.Ensures(Contract.Result<ComparerBuilder<T>>() != null);
      
      return AddComparison(expression, NullableConstant(comparison));
    }

    public ComparerBuilder<T> AddDefault<TProperty>(Expression<Func<T, TProperty>> expression) {
      Argument.NotNull(expression, "expression");
      Contract.Ensures(Contract.Result<ComparerBuilder<T>>() != null);
      
      return Add(expression, EqualityComparer<TProperty>.Default, Comparer<TProperty>.Default);
    }

    private static Expression<Func<T, T, bool>> BuildEquals(IEnumerable<Expression> items) {
      Argument.NotNull(items, "items");
      Contract.Ensures(Contract.Result<Expression<Func<T, T, bool>>>() != null);

      var expression = items.Aggregate(Expression.AndAlso);
      var body = IsValueType
        ? expression
        // return (object)x == (object)y || ((object)x != null && (object)y != null && expression);
        : Expression.OrElse(
            ReferenceEqual(Left, Right),
            Expression.AndAlso(
              Expression.AndAlso(IsNotNull(Left), IsNotNull(Right)),
              expression));
      return Expression.Lambda<Func<T, T, bool>>(body, Left, Right);
    }

    private static Expression<Func<T, int>> BuildGetHashCode(IEnumerable<Expression> items) {
      Argument.NotNull(items, "items");
      Contract.Ensures(Contract.Result<Expression<Func<T, int>>>() != null);

      var list = items as ICollection<Expression> ?? items.ToList();
      var expression = list.Skip(1).Select((item, index) => Tuple.Create(item, index + 1))
        .Aggregate(list.First(), (acc, item) =>
          Expression.ExclusiveOr(acc,
            Expression.Call(RotateRightDelegate.Method, item.Item1, Expression.Constant(item.Item2))));
      var body = IsValueType
        ? expression
        // return ((object)x == null) ? 0 : expression;
        : Expression.Condition(IsNull(Left), Zero, expression);
      return Expression.Lambda<Func<T, int>>(body, Left);
    }

    private static Expression<Func<T, T, int>> BuildCompare(IEnumerable<Expression> items) {
      Argument.NotNull(items, "items");
      Contract.Ensures(Contract.Result<Expression<Func<T, T, int>>>() != null);

      var reverse = items.Reverse().ToList();
      Expression seed = Expression.Return(Return, reverse.First());
      var expression = reverse.Skip(1).Aggregate(seed,
        (acc, value) => Expression.IfThenElse(
          Expression.NotEqual(Expression.Assign(Compare, value), Zero), ReturnCompare, acc));
      var body = IsValueType
        ? expression
        //if((object)x == (object)y) {
        //  return 0;
        //} else if((object)x == null) {
        //  return -1;
        //} else if((object)y == null) {
        //  return 1;
        //} else {
        //  return expression;
        //}//if
        : Expression.IfThenElse(ReferenceEqual(Left, Right), ReturnZero,
            Expression.IfThenElse(IsNull(Left), ReturnMinusOne,
              Expression.IfThenElse(IsNull(Right), ReturnOne, expression)));
      var block = Expression.Block(CompareValiables, body, LabelZero);
      return Expression.Lambda<Func<T, T, int>>(block, Left, Right);
    }

    public IEqualityComparer<T> ToEqualityComparer() {
      Contract.Ensures(Contract.Result<IEqualityComparer<T>>() != null);

      if(EqualsExpressions.Count == 0 || GetHashCodeExpressions.Count == 0) {
        return Comparers.True<T>();
      }//if

      var equals = BuildEquals(EqualsExpressions);
      var hashCode = BuildGetHashCode(GetHashCodeExpressions);
      return Comparers.Create(equals.Compile(), hashCode.Compile());
    }

    public IComparer<T> ToComparer() {
      Contract.Ensures(Contract.Result<IComparer<T>>() != null);

      if(CompareExpressions.Count == 0) {
        return Comparers.Zero<T>();
      }//if

      var compare = BuildCompare(CompareExpressions);
      return Comparers.Create(compare.Compile());
    }
  }
}